//
//  QueryTest_Main.m
//  CouchbaseLite
//
//  Copyright (c) 2017 Couchbase, Inc All rights reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

#import "QueryTest.h"
#import "CBLQuery+Internal.h"
#import "CBLQuery+JSON.h"
#import "CBLQueryBuilder.h"
#import "CBLQuerySelectResult.h"
#import "CBLQueryDataSource.h"
#import "CBLQueryOrdering.h"
#import "CBLQueryResultArray.h"
#import "CBLValueExpression.h"
#import "CBLQueryExpression+Internal.h"
#import "CBLUnaryExpression.h"
#import "Foundation+CBL.h"
#import "CollectionUtils.h"

#ifdef DEBUG
#import "CBLQueryObserver.h"
#endif

@interface QueryTest_Main : QueryTest

@end

@implementation QueryTest_Main

- (void) tearDown {
#ifdef DEBUG
    [CBLQueryObserver setC4QueryObserverCallbackDelayInterval: 0.0];
#endif
    [super tearDown];
}

#pragma mark - Where

- (void) testNoWhereQuery {
    [self loadJSONResource: @"names_100"];
    
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID, kSEQUENCE]
                                     from: kDATA_SRC_DB];
    Assert(q);
    
    uint64_t numRows = [self verifyQuery: q randomAccess: YES
                                    test:^(uint64_t n, CBLQueryResult *r) {
        NSString* docID = [r valueAtIndex: 0];
        NSString* expectedID = [NSString stringWithFormat: @"doc-%03llu", n];
        AssertEqualObjects(docID, expectedID);
        
        NSUInteger seq = [[r valueAtIndex: 1] unsignedIntegerValue];
        AssertEqual(seq, n);
        
        NSError* error;
        CBLDocument* doc = [self.defaultCollection documentWithID: docID error: &error];
        AssertEqualObjects(doc.id, expectedID);
        AssertEqual(doc.sequence, n);
    }];
    
    AssertEqual(numRows, 100llu);
}

- (void) testWhereComparison {
    CBLQueryExpression* n1 = [CBLQueryExpression property: @"number1"];
    NSArray* cases = @[
                       @[[n1 lessThan: [CBLQueryExpression integer: 3]], @"number1 < 3"],
                       @[[n1 lessThanOrEqualTo: [CBLQueryExpression integer: 3]], @"number1 <= 3"],
                       @[[n1 greaterThan: [CBLQueryExpression integer: 6]], @"number1 > 6"],
                       @[[n1 greaterThanOrEqualTo: [CBLQueryExpression integer: 6]], @"number1 >= 6"],
                       @[[n1 equalTo: [CBLQueryExpression integer: 7]], @"number1 == 7"],
                       ];
    NSArray* numbers = [self loadNumbers: 10];
    [self runTestWithNumbers: numbers cases: cases];
}

- (void) testWhereArithmetic {
    CBLQueryExpression* n1 = [CBLQueryExpression property: @"number1"];
    CBLQueryExpression* n2 = [CBLQueryExpression property: @"number2"];
    NSArray* cases = @[
                       @[[[n1 multiply: [CBLQueryExpression integer: 2]] greaterThan: [CBLQueryExpression integer: 8]], @"(number1 * 2) > 8"],
                       @[[[n1 divide: [CBLQueryExpression integer: 2]] greaterThan: [CBLQueryExpression integer: 3]], @"(number1 / 2) > 3"],
                       @[[[n1 modulo: [CBLQueryExpression integer: 2]] equalTo: [CBLQueryExpression integer: 0]], @"modulus:by:(number1, 2) == 0"],
                       @[[[n1 add: [CBLQueryExpression integer: 5]] greaterThan: [CBLQueryExpression integer: 10]], @"(number1 + 5) > 10"],
                       @[[[n1 subtract: [CBLQueryExpression integer: 5]] greaterThan: [CBLQueryExpression integer: 0]], @"(number1 - 5) > 0"],
                       @[[[n1 multiply: n2] greaterThan: [CBLQueryExpression integer: 10]], @"(number1 * number2) > 10"],
                       @[[[n2 divide: n1] greaterThan: [CBLQueryExpression integer: 3]], @"(number2 / number1) > 3"],
                       @[[[n2 modulo: n1] equalTo: [CBLQueryExpression integer: 0]], @"modulus:by:(number2, number1) == 0"],
                       @[[[n1 add: n2] equalTo: [CBLQueryExpression integer: 10]], @"(number1 + number2) == 10"],
                       @[[[n1 subtract: n2] greaterThan: [CBLQueryExpression integer: 0]], @"(number1 - number2) > 0"]
                       ];
    NSArray* numbers = [self loadNumbers: 10];
    [self runTestWithNumbers: numbers cases: cases];
}

- (void) testWhereAndOr {
    CBLQueryExpression* n1 = [CBLQueryExpression property: @"number1"];
    CBLQueryExpression* n2 = [CBLQueryExpression property: @"number2"];
    NSArray* cases = @[
                       @[[[n1 greaterThan: [CBLQueryExpression integer: 3]] andExpression: [n2 greaterThan: [CBLQueryExpression integer: 3]]], @"number1 > 3 AND number2 > 3"],
                       @[[[n1 lessThan: [CBLQueryExpression integer: 3]] orExpression: [n2 lessThan: [CBLQueryExpression integer: 3]]], @"number1 < 3 OR number2 < 3"]
                       ];
    NSArray* numbers = [self loadNumbers: 10];
    [self runTestWithNumbers: numbers cases: cases];
}

- (void) testWhereNullOrMissing {
    // https://github.com/couchbase/couchbase-lite-ios/issues/1670
    CBLMutableDocument* doc1 = [self createDocument: @"doc1"];
    [doc1 setValue: @"Scott" forKey: @"name"];
    [doc1 setValue: nil forKey: @"address"];
    [self saveDocument: doc1 collection: self.defaultCollection];
    
    CBLMutableDocument* doc2 = [self createDocument: @"doc2"];
    [doc2 setValue: @"Scott" forKey: @"name"];
    [doc2 setValue: @"123 1st ave." forKey: @"address"];
    [doc2 setValue: @(20) forKey: @"age"];
    [self saveDocument: doc2 collection: self.defaultCollection];
    
    CBLQueryExpression* name = [CBLQueryExpression property: @"name"];
    CBLQueryExpression* address = [CBLQueryExpression property: @"address"];
    CBLQueryExpression* age = [CBLQueryExpression property: @"age"];
    CBLQueryExpression* work = [CBLQueryExpression property: @"work"];
    
    NSArray* tests = @[
                       @[[name isNotValued],         @[]],
                       @[[name isValued],            @[doc1, doc2]],
                       @[[address isNotValued],      @[doc1]],
                       @[[address isValued],         @[doc2]],
                       @[[age isNotValued],          @[doc1]],
                       @[[age isValued],             @[doc2]],
                       @[[work isNotValued],         @[doc1, doc2]],
                       @[[work isValued],            @[]],
                       ];
    
    for (NSArray* test in tests) {
        CBLQueryExpression* exp = test[0];
        NSArray* expectedDocs = test[1];
        CBLQuery *q = [CBLQueryBuilder select: @[kDOCID]
                                         from: kDATA_SRC_DB
                                        where: exp];
        uint64_t numRows = [self verifyQuery: q randomAccess: YES
                                        test: ^(uint64_t n, CBLQueryResult* r) {
            
            if (expectedDocs.count <= n) {
                NSString* documentID = [r valueAtIndex: 0];
                CBLMutableDocument* expDoc = expectedDocs[(NSUInteger)(n-1)];
                AssertEqualObjects(expDoc.id, documentID, @"Failed case: %@", exp);
            }
        }];
        
        AssertEqual((int)numRows, (int)expectedDocs.count, @"Failed case: %@", exp);
    }
}

- (void) testWhereIs {
    NSError* error;
    CBLMutableDocument* doc1 = [[CBLMutableDocument alloc] init];
    [doc1 setValue: @"string" forKey: @"string"];
    Assert([self.defaultCollection saveDocument: doc1 error: &error], @"Error when creating a document: %@", error);
    
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: [[CBLQueryExpression property: @"string"] is: [CBLQueryExpression string: @"string"]]];
    
    Assert(q);
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        
        NSError* err;
        CBLDocument* doc = [self.defaultCollection documentWithID: [r valueAtIndex: 0] error: &err];
        AssertEqualObjects(doc.id, doc1.id);
        AssertEqualObjects([doc valueForKey: @"string"], @"string");
    }];
    
    AssertEqual(numRows, 1u);
    
    q = [CBLQueryBuilder select: @[kDOCID]
                           from: kDATA_SRC_DB
                          where: [[CBLQueryExpression property: @"string"] isNot: [CBLQueryExpression string: @"string1"]]];
    
    Assert(q);
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        NSError* err;
        CBLDocument* doc = [self.defaultCollection documentWithID: [r valueAtIndex: 0] error: &err];
        AssertEqualObjects(doc.id, doc1.id);
        AssertEqualObjects([doc valueForKey: @"string"], @"string");
    }];
    
    AssertEqual(numRows, 1u);
}

- (void) testWhereBetween {
    CBLQueryExpression* n1 = [CBLQueryExpression property: @"number1"];
    NSArray* cases = @[
                       @[[n1 between: [CBLQueryExpression integer: 3] and: [CBLQueryExpression integer: 7]], @"number1 BETWEEN {3,7}"]
                       ];
    NSArray* numbers = [self loadNumbers: 10];
    [self runTestWithNumbers: numbers cases: cases];
}

- (void) testWhereIn {
    [self loadJSONResource: @"names_100"];
    
    NSArray* expected = @[@"Marcy", @"Margaretta", @"Margrett", @"Marlen", @"Maryjo"];
    NSArray* names = @[[CBLQueryExpression string: @"Marcy"],
                       [CBLQueryExpression string: @"Margaretta"],
                       [CBLQueryExpression string: @"Margrett"],
                       [CBLQueryExpression string: @"Marlen"],
                       [CBLQueryExpression string: @"Maryjo"]];
    CBLQueryExpression* firstName = [CBLQueryExpression property: @"name.first"];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: [firstName in: names]
                                  orderBy: @[[CBLQuerySortOrder property: @"name.first"]]];
    uint64_t numRows = [self verifyQuery: q randomAccess: YES
                                    test:^(uint64_t n, CBLQueryResult* r)
                        {
                            NSError* error;
                            CBLDocument* doc = [self.defaultCollection documentWithID: [r valueAtIndex: 0] error: &error];
                            NSString* first = [[doc valueForKey: @"name"] valueForKey: @"first"];
                            AssertEqualObjects(first, expected[(NSUInteger)(n-1)]);
                        }];
    AssertEqual((int)numRows, (int)expected.count);
}

- (void) testWhereLike {
    [self loadJSONResource: @"names_100"];
    
    CBLQueryExpression* where = [[CBLQueryExpression property: @"name.first"] like: [CBLQueryExpression string: @"%Mar%"]];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: where
                                  orderBy: @[[[CBLQueryOrdering property: @"name.first"] ascending]]];
    
    NSMutableArray* firstNames = [NSMutableArray array];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: NO
                                    test:^(uint64_t n, CBLQueryResult* r) {
        NSError* error;
        CBLDocument* doc = [self.defaultCollection documentWithID: [r valueAtIndex: 0] error: &error];
        NSString* firstName = [[doc valueForKey:@"name"] valueForKey: @"first"];
        if (firstName) [firstNames addObject: firstName];
    }];
    
    AssertEqual(numRows, 5u);
    AssertEqual(firstNames.count, 5u);
}

- (void) testWhereRegex {
    [self loadJSONResource: @"names_100"];
    
    CBLQueryExpression* where = [[CBLQueryExpression property: @"name.first"] regex: [CBLQueryExpression string: @"^Mar.*"]];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: where
                                  orderBy: @[[[CBLQueryOrdering property: @"name.first"] ascending]]];
    
    NSMutableArray* firstNames = [NSMutableArray array];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: NO
                                    test:^(uint64_t n, CBLQueryResult* r) {
        NSError* error;
        CBLDocument* doc = [self.defaultCollection documentWithID: [r valueAtIndex: 0] error: &error];
        NSString* firstName = [[doc valueForKey:@"name"] valueForKey: @"first"];
        if (firstName) [firstNames addObject: firstName];
    }];
    
    AssertEqual(numRows, 5u);
    AssertEqual(firstNames.count, 5u);
}

- (void) testWhereFullTextFunctionMatch {
    [self loadJSONResource: @"sentences"];

    CBLQuerySelectResult* S_SENTENCE = [CBLQuerySelectResult property: @"sentence"];
    
    NSError* error;
    CBLFullTextIndex* index = [CBLIndexBuilder fullTextIndexWithItems: @[[CBLFullTextIndexItem property: @"sentence"]]];
    Assert([self.defaultCollection createIndex: index name: @"sentence" error: &error],
           @"Error when creating the index: %@", error);
    
    id plainIndex = [CBLQueryExpression fullTextIndex: @"sentence"];
    
    CBLQueryOrdering* order = [[CBLQueryOrdering expression: [CBLQueryFullTextFunction rankWithIndex: plainIndex]] descending];
    
    CBLQueryExpression* exp = [CBLQueryFullTextFunction matchWithIndex: plainIndex
                                                                 query: @"'Dummie woman'"];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID, S_SENTENCE]
                                     from: kDATA_SRC_DB
                                    where: exp
                                  orderBy: @[order]];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test:^(uint64_t n, CBLQueryResult* r) { }];
    AssertEqual(numRows, 2u);
}


#pragma mark - Select

- (void) testSelectDistinct {
    NSError* error;
    CBLMutableDocument* doc1 = [[CBLMutableDocument alloc] init];
    [doc1 setValue: @(20) forKey: @"number"];
    Assert([self.defaultCollection saveDocument: doc1 error: &error], @"Error when creating a document: %@", error);
    
    CBLMutableDocument* doc2 = [[CBLMutableDocument alloc] init];
    [doc2 setValue: @(20) forKey: @"number"];
    Assert([self.defaultCollection saveDocument: doc2 error: &error], @"Error when creating a document: %@", error);
    
    CBLQueryExpression* NUMBER  = [CBLQueryExpression property: @"number"];
    CBLQuerySelectResult* S_NUMBER = [CBLQuerySelectResult expression: NUMBER];
    
    CBLQuery* q = [CBLQueryBuilder selectDistinct: @[S_NUMBER]
                                             from: kDATA_SRC_DB];
    Assert(q);
    uint64_t numRows = [self verifyQuery: q randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r)
                        {
                            AssertEqual([r integerAtIndex: 0], 20);
                        }];
    AssertEqual(numRows, 1u);
    
    CBLMutableDocument* doc3 = [[CBLMutableDocument alloc] init];
    [doc3 setValue: @(30) forKey: @"number"];
    Assert([self.defaultCollection saveDocument: doc3 error: &error], @"Error when creating a document: %@", error);
    
    CBLMutableDocument* doc4 = [[CBLMutableDocument alloc] init];
    [doc4 setValue: @(25) forKey: @"number"];
    Assert([self.defaultCollection saveDocument: doc4 error: &error], @"Error when creating a document: %@", error);
    
    CBLQueryExpression* whereExp = [[CBLQueryExpression property: @"number"] lessThan: [CBLQueryExpression value: @(30)]];
    q = [CBLQueryBuilder selectDistinct: @[S_NUMBER]
                                   from: kDATA_SRC_DB
                                  where: whereExp];
    Assert(q);
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual([r count], 1);
    }];
    
    AssertEqual(numRows, 2u);
}

- (void) testSelectAll {
    [self loadNumbers: 100];
    
    CBLQueryExpression* NUMBER1 = [CBLQueryExpression property: @"number1"];
    CBLQuerySelectResult* S_NUMBER1 = [CBLQuerySelectResult expression: NUMBER1];
    CBLQuerySelectResult* S_STAR = [CBLQuerySelectResult all];
    
    CBLQueryExpression* TESTDB_NUMBER1 = [CBLQueryExpression property: @"number1" from: @"testdb"];
    CBLQuerySelectResult* S_TESTDB_NUMBER1 = [CBLQuerySelectResult expression: TESTDB_NUMBER1];
    CBLQuerySelectResult* S_TESTDB_STAR = [CBLQuerySelectResult allFrom: @"testdb"];
    
    // SELECT *
    CBLQuery* q = [CBLQueryBuilder select: @[S_STAR]
                                     from: kDATA_SRC_DB];
    
    uint64_t numRows = [self verifyQuery: q randomAccess: YES test: ^(uint64_t n, CBLQueryResult* r)
                        {
                            AssertEqual(r.count, 1u);
                            CBLMutableDictionary* a1 = [r valueAtIndex: 0];
                            CBLMutableDictionary* a2 = [r valueForKey: self.defaultCollection.name];
                            AssertEqual([a1 integerForKey: @"number1"], (NSInteger)n);
                            AssertEqual([a1 integerForKey: @"number2"], (NSInteger)(100 - n));
                            AssertEqual([a2 integerForKey: @"number1"], (NSInteger)n);
                            AssertEqual([a2 integerForKey: @"number2"], (NSInteger)(100 - n));
                        }];
    AssertEqual(numRows, 100u);
    
    // SELECT testdb.*
    q = [CBLQueryBuilder select: @[S_TESTDB_STAR]
                           from: [CBLQueryDataSource collection: self.defaultCollection as: @"testdb"]];
    
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual(r.count, 1u);
        CBLMutableDictionary* a1 = [r valueAtIndex: 0];
        CBLMutableDictionary* a2 = [r valueForKey: @"testdb"];
        AssertEqual([a1 integerForKey: @"number1"], (NSInteger)n);
        AssertEqual([a1 integerForKey: @"number2"], (NSInteger)(100 - n));
        AssertEqual([a2 integerForKey: @"number1"], (NSInteger)n);
        AssertEqual([a2 integerForKey: @"number2"], (NSInteger)(100 - n));
    }];
    
    AssertEqual(numRows, 100u);
    
    // SELECT *, number1
    q = [CBLQueryBuilder select: @[S_STAR, S_NUMBER1]
                           from: kDATA_SRC_DB];
    
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r){
        AssertEqual(r.count, 2u);
        CBLMutableDictionary* a1 = [r valueAtIndex: 0];
        CBLMutableDictionary* a2 = [r valueForKey: self.defaultCollection.name];
        AssertEqual([a1 integerForKey: @"number1"], (NSInteger)n);
        AssertEqual([a1 integerForKey: @"number2"], (NSInteger)(100 - n));
        AssertEqual([a2 integerForKey: @"number1"], (NSInteger)n);
        AssertEqual([a2 integerForKey: @"number2"], (NSInteger)(100 - n));
        AssertEqual([r integerAtIndex: 1], (NSInteger)n);
        AssertEqual([r integerForKey: @"number1"], (NSInteger)n);
    }];
    
    AssertEqual(numRows, 100u);
    
    // SELECT testdb.*, testdb.number1
    q = [CBLQueryBuilder select: @[S_TESTDB_STAR, S_TESTDB_NUMBER1]
                           from: [CBLQueryDataSource collection: self.defaultCollection as: @"testdb"]];
    
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        
        AssertEqual(r.count, 2u);
        CBLMutableDictionary* a1 = [r valueAtIndex: 0];
        CBLMutableDictionary* a2 = [r valueForKey: @"testdb"];
        AssertEqual([a1 integerForKey: @"number1"], (NSInteger)n);
        AssertEqual([a1 integerForKey: @"number2"], (NSInteger)(100 - n));
        AssertEqual([a2 integerForKey: @"number1"], (NSInteger)n);
        AssertEqual([a2 integerForKey: @"number2"], (NSInteger)(100 - n));
        AssertEqual([r integerAtIndex: 1], (NSInteger)n);
        AssertEqual([r integerForKey: @"number1"], (NSInteger)n);
    }];
    
    AssertEqual(numRows, 100u);
}

- (void) testDatabaseAliasWithMultipleSources {
    [self loadNumbers: 100];
    
    CBLMutableDocument* joinme = [[CBLMutableDocument alloc] initWithID: @"joinme"];
    [joinme setValue: @42 forKey: @"theone"];
    [self saveDocument: joinme collection: self.defaultCollection];
    
    CBLMutableDocument* joinmeCopy = [[CBLMutableDocument alloc] initWithID: @"joinmeCopy"];
    [joinmeCopy setValue: @42 forKey: @"theone"];
    [self saveDocument: joinmeCopy collection: self.defaultCollection];
    
    CBLQueryExpression* propNum1 = [CBLQueryExpression property: @"number1" from: @"main"];
    CBLQueryExpression* propTheOne = [CBLQueryExpression property: @"theone" from: @"secondary"];
    
    CBLQueryJoin* join = [CBLQueryJoin join: [CBLQueryDataSource collection: self.defaultCollection as: @"secondary"]
                                         on: [propNum1 equalTo: propTheOne]];
    
    CBLQuery* q = [CBLQueryBuilder selectDistinct: @[[CBLQuerySelectResult allFrom: @"main"],
                                                     [CBLQuerySelectResult allFrom: @"secondary"]]
                                             from: [CBLQueryDataSource collection: self.defaultCollection as: @"main"]
                                             join: @[join]];
    Assert(q);
    uint64_t numRows = 0;
    numRows = [self verifyQuery: q randomAccess: YES test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqualObjects([r dictionaryAtIndex: 0], [r dictionaryForKey: @"main"]);
        AssertEqualObjects([r dictionaryAtIndex: 1], [r dictionaryForKey: @"secondary"]);
    }];
    AssertEqual(numRows, 1u);
}


#pragma mark - OrderBy/GroupBy

- (void) testOrderBy {
    [self loadJSONResource: @"names_100"];
    
    for (id ascending in @[@(YES), @(NO)]) {
        BOOL isAscending = [ascending boolValue];
        
        CBLQueryOrdering* order;
        CBLQueryOrdering* orderByState;
        if (isAscending) {
            order = [[CBLQueryOrdering property: @"name.first"] ascending];
            orderByState = [[CBLQueryOrdering property: @"contact.address.state"] ascending];
        } else {
            order = [[CBLQueryOrdering property: @"name.first"] descending];
            orderByState = [[CBLQueryOrdering property: @"contact.address.state"] descending];
        }
        
        CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                         from: kDATA_SRC_DB
                                        where: nil
                                      orderBy: @[order]];
        Assert(q);
        
        NSMutableArray* firstNames = [NSMutableArray array];
        uint64_t numRows = [self verifyQuery: q
                                randomAccess: NO
                                        test: ^(uint64_t n, CBLQueryResult* r) {
            NSError* error;
            CBLDocument* doc = [self.defaultCollection documentWithID: [r valueAtIndex: 0] error: &error];
            NSString* firstName = [[doc valueForKey:@"name"] valueForKey: @"first"];
            if (firstName) [firstNames addObject: firstName];
        }];
        
        AssertEqual(numRows, 100llu);
        AssertEqual(numRows, firstNames.count);
        
        NSSortDescriptor* desc = [NSSortDescriptor sortDescriptorWithKey: nil
                                                               ascending: isAscending
                                                                selector: @selector(localizedCompare:)];
        AssertEqualObjects(firstNames, [firstNames sortedArrayUsingDescriptors: @[desc]]);
        
        // selectDistinctFromWhereOrderBy
        CBLQuery* distinctQ = [CBLQueryBuilder selectDistinct: @[[CBLQuerySelectResult property: @"contact.address.state"]]
                                                         from: kDATA_SRC_DB
                                                        where: nil
                                                      orderBy: @[orderByState]];
        Assert(distinctQ);
        NSMutableArray* distinctStates = [NSMutableArray array];
        numRows = [self verifyQuery: distinctQ randomAccess: NO
                               test: ^(uint64_t n, CBLQueryResult* r)
                   {
                       NSString* state = [r valueAtIndex: 0];
                       if (state)
                           [distinctStates addObject: state];
                       
                   }];
        AssertEqual(numRows, 42llu);
        AssertEqual(numRows, distinctStates.count);
        AssertEqualObjects(distinctStates.firstObject, isAscending ? @"AL" : @"WV");
    }
}

- (void) testGroupBy {
    NSArray* expectedStates  = @[@"AL",    @"CA",    @"CO",    @"FL",    @"IA"];
    NSArray* expectedCounts  = @[@1,       @6,       @1,       @1,       @3];
    NSArray* expectedMaxZips = @[@"35243", @"94153", @"81223", @"33612", @"50801"];
    
    [self loadJSONResource: @"names_100"];
    
    CBLQueryExpression* STATE  = [CBLQueryExpression property: @"contact.address.state"];
    CBLQueryExpression* COUNT  = [CBLQueryFunction count: [CBLQueryExpression integer: 1]];
    CBLQueryExpression* ZIP    = [CBLQueryExpression property: @"contact.address.zip"];
    CBLQueryExpression* MAXZIP = [CBLQueryFunction max: ZIP];
    CBLQueryExpression* GENDER = [CBLQueryExpression property: @"gender"];
    
    NSArray* results = @[[CBLQuerySelectResult expression: STATE],
                         [CBLQuerySelectResult expression: COUNT],
                         [CBLQuerySelectResult expression: MAXZIP]];
    
    CBLQuery* q = [CBLQueryBuilder select: results
                                     from: kDATA_SRC_DB
                                    where: [GENDER equalTo: [CBLQueryExpression string: @"female"]]
                                  groupBy: @[STATE]
                                   having: nil
                                  orderBy: @[[CBLQueryOrdering expression: STATE]]
                                    limit: nil];
    
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        NSString* state = [r valueAtIndex: 0];
        NSInteger count = [[r valueAtIndex: 1] integerValue];
        NSString* maxZip = [r valueAtIndex: 2];
        // Log(@"State = %@, count = %d, maxZip = %@", state, (int)count, maxZip);
        if (n-1 < expectedStates.count) {
            AssertEqualObjects(state,  expectedStates[(NSUInteger)(n-1)]);
            AssertEqual       (count,  [expectedCounts[(NSUInteger)(n-1)] integerValue]);
            AssertEqualObjects(maxZip, expectedMaxZips[(NSUInteger)(n-1)]);
        }
    }];
    
    AssertEqual(numRows, 31u);
    
    // With HAVING:
    expectedStates  = @[@"CA",    @"IA",     @"IN"];
    expectedCounts  = @[@6,       @3,        @2];
    expectedMaxZips = @[@"94153", @"50801",  @"47952"];
    
    q = [CBLQueryBuilder select: results
                           from: kDATA_SRC_DB
                          where: [GENDER equalTo: [CBLQueryExpression string: @"female"]]
                        groupBy: @[STATE]
                         having: [COUNT greaterThan: [CBLQueryExpression integer: 1]]
                        orderBy: @[[CBLQueryOrdering expression: STATE]]
                          limit: nil];
    
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        NSString* state = [r valueAtIndex: 0];
        NSInteger count = [[r valueAtIndex: 1] integerValue];
        NSString* maxZip = [r valueAtIndex: 2];
        // Log(@"State = %@, count = %d, maxZip = %@", state, (int)count, maxZip);
        if (n-1 < expectedStates.count) {
            AssertEqualObjects(state,  expectedStates[(NSUInteger)(n-1)]);
            AssertEqual       (count,  [expectedCounts[(NSUInteger)(n-1)] integerValue]);
            AssertEqualObjects(maxZip, expectedMaxZips[(NSUInteger)(n-1)]);
        }
    }];
    AssertEqual(numRows, 15u);
}

- (void) testSelectFromWhereGroupBy {
    [self loadJSONResource: @"names_100"];
    
    CBLQueryExpression* COUNT  = [CBLQueryFunction count: [CBLQueryExpression integer: 1]];
    CBLQueryExpression* GENDER = [CBLQueryExpression property: @"gender"];
    CBLQueryExpression* STATE  = [CBLQueryExpression property: @"contact.address.state"];
    
    NSArray* results = @[[CBLQuerySelectResult expression: STATE]];
    
    // selectFromWhereGroupBy
    CBLQuery* q = [CBLQueryBuilder select: results
                                     from: kDATA_SRC_DB
                                    where: [GENDER equalTo: [CBLQueryExpression string: @"female"]]
                                  groupBy: @[STATE]];
    NSError* error;
    CBLQueryResultSet* rs = [q execute: &error];
    AssertEqual([rs allResults].count, 31u);
    
    // selectFromWhereGroupByHaving
    q = [CBLQueryBuilder select: results
                           from: kDATA_SRC_DB
                          where: [GENDER equalTo: [CBLQueryExpression string: @"female"]]
                        groupBy: @[STATE]
                         having: [COUNT greaterThan: [CBLQueryExpression integer: 2]]];
    
    rs = [q execute: &error];
    AssertEqual([rs allResults].count, 5u);
}

- (void) testSelectDistinctFromWhereGroupBy {
    NSError* error;
    CBLMutableDocument* doc1 = [[CBLMutableDocument alloc] init];
    [doc1 setValue: @(20) forKey: @"number"];
    [doc1 setValue: @"Tom" forKey: @"name"];
    Assert([self.defaultCollection saveDocument: doc1 error: &error], @"Error when creating a document: %@", error);
    
    CBLMutableDocument* doc2 = [[CBLMutableDocument alloc] init];
    [doc2 setValue: @(20) forKey: @"number"];
    [doc2 setValue: @"Bob" forKey: @"name"];
    Assert([self.defaultCollection saveDocument: doc2 error: &error], @"Error when creating a document: %@", error);
    
    CBLMutableDocument* doc3 = [[CBLMutableDocument alloc] init];
    [doc3 setValue: @(30) forKey: @"number"];
    [doc3 setValue: @"Alice" forKey: @"name"];
    Assert([self.defaultCollection saveDocument: doc3 error: &error], @"Error when creating a document: %@", error);
    
    CBLMutableDocument* doc4 = [[CBLMutableDocument alloc] init];
    [doc4 setValue: @(25) forKey: @"number"];
    [doc4 setValue: @"Bob" forKey: @"name"];
    Assert([self.defaultCollection saveDocument: doc4 error: &error], @"Error when creating a document: %@", error);
    
    CBLMutableDocument* doc5 = [[CBLMutableDocument alloc] init];
    [doc5 setValue: @(15) forKey: @"number"];
    [doc5 setValue: @"Adam" forKey: @"name"];
    Assert([self.defaultCollection saveDocument: doc5 error: &error], @"Error when creating a document: %@", error);
    
    CBLQueryExpression* COUNT  = [CBLQueryFunction count: [CBLQueryExpression integer: 1]];
    CBLQueryExpression* NUMBER  = [CBLQueryExpression property: @"number"];
    CBLQueryExpression* NAME  = [CBLQueryExpression property: @"name"];
    CBLQuerySelectResult* S_NUMBER = [CBLQuerySelectResult expression: NUMBER];
    CBLQuerySelectResult* S_NAME = [CBLQuerySelectResult expression: NAME];
    CBLQuerySelectResult* S_COUNT = [CBLQuerySelectResult expression: COUNT];
    
    // selectDistinctFromWhereGroupBy
    CBLQuery* q = [CBLQueryBuilder selectDistinct: @[S_NUMBER, S_NAME]
                                             from: kDATA_SRC_DB
                                            where: nil
                                          groupBy: @[NAME]];
    Assert(q);
    CBLQueryResultSet* rs = [q execute: &error];
    AssertEqual([rs allResults].count, 4u);
    
    //selectDistinctFromWhereGroupByHaving
    q = [CBLQueryBuilder selectDistinct: @[S_COUNT, S_NUMBER, S_NAME]
                                   from: kDATA_SRC_DB
                                  where: nil
                                groupBy: @[NAME]
                                 having: [COUNT greaterThan: [CBLQueryExpression integer: 1]]];
    Assert(q);
    rs = [q execute: &error];
    AssertEqual([rs allResults].count, 1u); // only doc with name = Bob will pass.
    
    // selectDistinctFromWhereGroupByHavingOrderByLimit
    q = [CBLQueryBuilder selectDistinct: @[S_NUMBER, S_NAME]
                                   from: kDATA_SRC_DB
                                  where: nil
                                groupBy: @[NAME]
                                 having: [COUNT lessThan: [CBLQueryExpression integer: 2]]
                                orderBy: @[[CBLQuerySortOrder property: @"name"]]
                                  limit: [CBLQueryLimit limit: [CBLQueryExpression integer: 2]]];
    Assert(q);
    rs = [q execute: &error];
    NSArray* allResults = [rs allResults];
    AssertEqual(allResults.count, 2u);
    AssertEqualObjects([allResults.firstObject valueForKey: @"name"], @"Adam");
}

#pragma mark - Query Params

- (void) testQueryParameters {
    [self loadStudents];
    
    CBLQueryExpression* PARAM_CITY = [CBLQueryExpression parameterNamed: @"city"];
    CBLQueryExpression* PARAM_CODE = [CBLQueryExpression parameterNamed: @"code"];
    CBLQueryExpression* PARAM_YEAR = [CBLQueryExpression parameterNamed: @"year"];
    CBLQueryExpression* PARAM_ID = [CBLQueryExpression parameterNamed: @"id"];
    CBLQueryExpression* PARAM_SCORE = [CBLQueryExpression parameterNamed: @"score"];
    CBLQueryExpression* PARAM_GPA = [CBLQueryExpression parameterNamed: @"gpa"];
    CBLQueryExpression* PARAM_IS_FULLTIME = [CBLQueryExpression parameterNamed: @"isFullTime"];
    CBLQueryExpression* PARAM_START_DATE = [CBLQueryExpression parameterNamed: @"startDate"];
    
    CBLQueryExpression* qCity = [[CBLQueryExpression property: @"city"] equalTo: PARAM_CITY];
    CBLQueryExpression* qCode = [[CBLQueryExpression property: @"code"] greaterThan: PARAM_CODE];
    CBLQueryExpression* qYear = [[CBLQueryExpression property: @"year"] greaterThan: PARAM_YEAR];
    CBLQueryExpression* qID = [[CBLQueryExpression property: @"id"] greaterThan: PARAM_ID];
    CBLQueryExpression* qScore = [[CBLQueryExpression property: @"score"] greaterThan: PARAM_SCORE];
    CBLQueryExpression* qGPA = [[CBLQueryExpression property: @"gpa"]
                                greaterThanOrEqualTo: PARAM_GPA];
    CBLQueryExpression* qIsFullTime = [[CBLQueryExpression property: @"isFullTime"]
                                       equalTo: PARAM_IS_FULLTIME];
    CBLQueryExpression* qStartDate = [[CBLQueryExpression property: @"startDate"]
                                      greaterThan: PARAM_START_DATE];
    CBLQueryExpression* where = [[[[[[[qCity andExpression: qCode]
                                      andExpression: qYear]
                                     andExpression: qID] andExpression: qScore]
                                   andExpression: qGPA]
                                  andExpression: qIsFullTime]
                                 andExpression: qStartDate];
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult all]]
                                     from: kDATA_SRC_DB
                                    where: where];
    
    CBLQueryParameters* params = [[CBLQueryParameters alloc] init];
    [params setString: @"santa clara" forName: @"city"];
    [params setNumber: @(100) forName: @"code"];
    [params setInteger: 2016  forName: @"year"];
    [params setLongLong: 123456791 forName: @"id"];
    [params setFloat: 60.00f forName: @"score"];
    [params setDouble: 4.0 forName: @"gpa"];
    [params setBoolean: YES forName: @"isFullTime"];
    NSDate* twoWeeksBack = [NSDate dateWithTimeIntervalSinceNow: -2 * 7 * 24 * 60 * 60];
    [params setDate: twoWeeksBack forName: @"startDate"];
    
    q.parameters = params;
    
    NSError* error;
    NSArray<CBLQueryResult*>* allObjects = [[q execute: &error] allObjects];
    AssertEqual(allObjects.count, 1u);
    AssertEqualObjects([[allObjects.firstObject valueAtIndex: 0] stringForKey: @"name"],
                       @"Casper");
}

- (void) testQueryArrayDictionaryParameters {
    // DOC 1
    NSError* error;
    CBLMutableDictionary* dict = [[CBLMutableDictionary alloc] initWithData: @{@"1": @"submitted",
                                                                               @"2": @"pending",
                                                                               @"3": @"pending" }];
    CBLMutableArray* phones = [[CBLMutableArray alloc] initWithData: @[@"920-456-7890",
                                                                       @"920-123-4566"]];
    CBLMutableDocument* doc1 = [[CBLMutableDocument alloc] init];
    [doc1 setString: @"Jason" forKey: @"name"];
    [doc1 setDictionary: dict forKey: @"assignments"];
    [doc1 setArray: phones forKey: @"phone"];
    Assert([self.defaultCollection saveDocument: doc1 error: &error], @"Error when creating a document: %@", error);
    
    // DOC 3
    dict = [[CBLMutableDictionary alloc] initWithData: @{@"1": @"submitted",
                                                         @"2": @"pending",
                                                         @"3": @"submitted" }];
    phones = [[CBLMutableArray alloc] initWithData: @[@"920-123-4567"]];
    CBLMutableDocument* doc3 = [[CBLMutableDocument alloc] init];
    [doc3 setString: @"Alice" forKey: @"name"];
    [doc3 setDictionary: dict forKey: @"assignments"];
    [doc3 setArray: phones forKey: @"phone"];
    Assert([self.defaultCollection saveDocument: doc3 error: &error], @"Error when creating a document: %@", error);
    
    CBLQueryExpression* PARAM_ASSIGNMENTS = [CBLQueryExpression parameterNamed: @"assignments"];
    CBLQueryExpression* PARAM_PHONE = [CBLQueryExpression parameterNamed: @"phone"];
    
    CBLQueryExpression* qAssignments = [[CBLQueryExpression property: @"assignments"]
                                        equalTo: PARAM_ASSIGNMENTS];
    CBLQueryExpression* qPhone = [[CBLQueryExpression property: @"phone"] equalTo: PARAM_PHONE];
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult all]]
                                     from: kDATA_SRC_DB
                                    where: [qAssignments andExpression: qPhone]];
    
    CBLQueryParameters* params = [[CBLQueryParameters alloc] init];
    [params setDictionary: dict forName: @"assignments"];
    [params setArray: phones  forName: @"phone"];
    
    q.parameters = [[CBLQueryParameters alloc] initWithParameters: params];
    
    NSArray<CBLQueryResult*>* allObjects = [[q execute: &error] allObjects];
    AssertEqual(allObjects.count, 1u);
    AssertEqualObjects([[allObjects.firstObject valueAtIndex: 0] stringForKey: @"name"],
                       @"Alice");
}

#pragma mark - Limit

- (void) testLimit {
    [self loadNumbers: 10];
    
    CBLQueryExpression* NUMBER1  = [CBLQueryExpression property: @"number1"];
    
    CBLQuery* q= [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: NUMBER1]]
                                    from: kDATA_SRC_DB
                                   where: nil groupBy: nil having: nil
                                 orderBy: @[[CBLQueryOrdering expression: NUMBER1]]
                                   limit: [CBLQueryLimit limit: [CBLQueryExpression integer: 5]]];
    
    NSArray* expectedNumbers = @[@1, @2, @3, @4, @5];
    uint64_t numRows = [self verifyQuery: q randomAccess: YES test: ^(uint64_t n, CBLQueryResult* r)
                        {
                            NSInteger number = [[r valueAtIndex: 0] integerValue];
                            AssertEqual(number, [expectedNumbers[(NSUInteger)(n-1)] integerValue]);
                        }];
    AssertEqual(numRows, 5u);
    
    q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: NUMBER1]]
                           from: kDATA_SRC_DB
                          where: nil groupBy: nil having: nil
                        orderBy: @[[CBLQueryOrdering expression: NUMBER1]]
                          limit: [CBLQueryLimit limit: [CBLQueryExpression parameterNamed: @"LIMIT_NUM"]]];
    
    CBLQueryParameters* params = [[CBLQueryParameters alloc] init];
    [params setValue: @3 forName: @"LIMIT_NUM"];
    q.parameters = params;
    
    expectedNumbers = @[@1, @2, @3];
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        NSInteger number = [[r valueAtIndex: 0] integerValue];
        AssertEqual(number, [expectedNumbers[(NSUInteger)(n-1)] integerValue]);
    }];
    
    AssertEqual(numRows, 3u);
}

- (void) testLimitOffset {
    [self loadNumbers: 10];
    
    CBLQueryExpression* NUMBER1  = [CBLQueryExpression property: @"number1"];
    
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: NUMBER1]]
                                     from: kDATA_SRC_DB
                                    where: nil groupBy: nil having: nil
                                  orderBy: @[[CBLQueryOrdering expression: NUMBER1]]
                                    limit: [CBLQueryLimit limit: [CBLQueryExpression integer: 5]
                                                         offset: [CBLQueryExpression integer: 3]]];
    
    NSArray* expectedNumbers = @[@4, @5, @6, @7, @8];
    uint64_t numRows = [self verifyQuery: q randomAccess: YES test: ^(uint64_t n, CBLQueryResult* r)
                        {
                            NSInteger number = [[r valueAtIndex: 0] integerValue];
                            AssertEqual(number, [expectedNumbers[(NSUInteger)(n-1)] integerValue]);
                        }];
    AssertEqual(numRows, 5u);
    
    q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: NUMBER1]]
                           from: kDATA_SRC_DB
                          where: nil groupBy: nil having: nil
                        orderBy: @[[CBLQueryOrdering expression: NUMBER1]]
                          limit: [CBLQueryLimit limit: [CBLQueryExpression parameterNamed: @"LIMIT_NUM"]
                                               offset: [CBLQueryExpression parameterNamed:@"OFFSET_NUM"]]];
    
    CBLQueryParameters* params = [[CBLQueryParameters alloc] init];
    [params setValue: @3 forName: @"LIMIT_NUM"];
    [params setValue: @5 forName: @"OFFSET_NUM"];
    q.parameters = params;
    
    expectedNumbers = @[@6, @7, @8];
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        NSInteger number = [[r valueAtIndex: 0] integerValue];
        AssertEqual(number, [expectedNumbers[(NSUInteger)(n-1)] integerValue]);
    }];
    
    AssertEqual(numRows, 3u);
}

#pragma mark - Functions

- (void) testAggregateFunctions {
    [self loadNumbers: 100];
    
    CBLQueryExpression* AVG = [CBLQueryFunction avg: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* CNT = [CBLQueryFunction count: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* MIN = [CBLQueryFunction min: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* MAX = [CBLQueryFunction max: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* SUM = [CBLQueryFunction sum: [CBLQueryExpression property: @"number1"]];
    
    NSArray* results = @[[CBLQuerySelectResult expression: AVG],
                         [CBLQuerySelectResult expression: CNT],
                         [CBLQuerySelectResult expression: MIN],
                         [CBLQuerySelectResult expression: MAX],
                         [CBLQuerySelectResult expression: SUM]];
    
    CBLQuery* q = [CBLQueryBuilder select: results
                                     from: kDATA_SRC_DB];
    
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual([[r valueAtIndex:0] doubleValue], 50.5);
        AssertEqual([[r valueAtIndex:1] integerValue], 100);
        AssertEqual([[r valueAtIndex:2] integerValue], 1);
        AssertEqual([[r valueAtIndex:3] integerValue], 100);
        AssertEqual([[r valueAtIndex:4] integerValue], 5050);
    }];
    
    AssertEqual(numRows, 1u);
}

- (void) testArrayFunctions {
    CBLMutableDocument* doc = [self createDocument:@"doc1"];
    CBLMutableArray* array = [[CBLMutableArray alloc] init];
    [array addValue: @"650-123-0001"];
    [array addValue: @"650-123-0002"];
    [doc setValue: array forKey: @"array"];
    [self saveDocument: doc collection: self.defaultCollection];
    
    CBLQueryExpression* ARRAY_LENGTH = [CBLQueryArrayFunction length:
                                        [CBLQueryExpression property: @"array"]];
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: ARRAY_LENGTH]]
                                     from: kDATA_SRC_DB];
    uint64_t numRows = [self verifyQuery: q randomAccess: YES test: ^(uint64_t n, CBLQueryResult* r)
                        {
                            AssertEqual([r integerAtIndex: 0], 2);
                        }];
    AssertEqual(numRows, 1u);
    
    CBLQueryExpression* ARRAY_CONTAINS1 = [CBLQueryArrayFunction contains: [CBLQueryExpression property: @"array"]
                                                                    value: [CBLQueryExpression string: @"650-123-0001"]];
    CBLQueryExpression* ARRAY_CONTAINS2 = [CBLQueryArrayFunction contains: [CBLQueryExpression property: @"array"]
                                                                    value: [CBLQueryExpression string: @"650-123-0003"]];
    q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: ARRAY_CONTAINS1],
                                   [CBLQuerySelectResult expression: ARRAY_CONTAINS2]]
                           from: kDATA_SRC_DB];
    
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual([r booleanAtIndex: 0], YES);
        AssertEqual([r booleanAtIndex: 1], NO);
    }];
    
    AssertEqual(numRows, 1u);
}

- (void) testMathFunctions {
    double num = 0.6;
    CBLMutableDocument* doc = [self createDocument:@"doc1"];
    [doc setValue: @(num) forKey: @"number"];
    [self saveDocument: doc collection: self.defaultCollection];
    
    NSArray* expectedValues = @[@(0.6),
                                @(acos(num)),
                                @(asin(num)),
                                @(atan(num)),
                                @(atan2(90.0, num)),
                                @(ceil(num)),
                                @(cos(num)),
                                @(num * 180.0 / M_PI),
                                @(exp(num)),
                                @(floor(num)),
                                @(log(num)),
                                @(log10(num)),
                                @(pow(num, 2)),
                                @(num * M_PI / 180.0),
                                @(round(num)),
                                @(round(num * 10.0) / 10.0),
                                @(1),
                                @(sin(num)),
                                @(sqrt(num)),
                                @(tan(num)),
                                @(trunc(num)),
                                @(trunc(num * 10.0) / 10.0)];
    
    CBLQueryExpression* p = [CBLQueryExpression property: @"number"];
    NSArray* functions = @[[CBLQueryFunction abs: p],
                           [CBLQueryFunction acos: p],
                           [CBLQueryFunction asin: p],
                           [CBLQueryFunction atan: p],
                           [CBLQueryFunction atan2: [CBLQueryExpression integer: 90] x: p],
                           [CBLQueryFunction ceil: p],
                           [CBLQueryFunction cos: p],
                           [CBLQueryFunction degrees: p],
                           [CBLQueryFunction exp: p],
                           [CBLQueryFunction floor: p],
                           [CBLQueryFunction ln: p],
                           [CBLQueryFunction log: p],
                           [CBLQueryFunction power: p exponent: [CBLQueryExpression integer: 2]],
                           [CBLQueryFunction radians: p],
                           [CBLQueryFunction round: p],
                           [CBLQueryFunction round: p digits: [CBLQueryExpression integer: 1]],
                           [CBLQueryFunction sign: p],
                           [CBLQueryFunction sin: p],
                           [CBLQueryFunction sqrt: p],
                           [CBLQueryFunction tan: p],
                           [CBLQueryFunction trunc: p],
                           [CBLQueryFunction trunc: p digits: [CBLQueryExpression integer: 1]]];
    
    int index = 0;
    for (CBLQueryExpression *f in functions) {
        CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: f]]
                                         from: kDATA_SRC_DB];
        
        uint64_t numRows = [self verifyQuery: q
                                randomAccess: YES
                                        test: ^(uint64_t n, CBLQueryResult* r) {
            double expected = [expectedValues[index] doubleValue];
            AssertEqual([r doubleAtIndex: 0], expected);
        }];
        
        AssertEqual(numRows, 1u);
        index++;
    }
}

- (void) testDivisionFunctionPrecision {
    CBLMutableDocument* doc = [self createDocument:@"doc1"];
    [doc setValue: @(5.0) forKey: @"key1"];
    [doc setValue: @(15.0) forKey: @"key2"];
    [doc setValue: @(5.5) forKey: @"key3"];
    [doc setValue: @(16.5) forKey: @"key4"];
    [self saveDocument: doc collection: self.defaultCollection];
    
    id withoutPrecision = [[CBLQueryExpression property: @"key1"]
                           divide: [CBLQueryExpression property: @"key2"]];
    id withPrecision = [[CBLQueryExpression property: @"key3"]
                        divide: [CBLQueryExpression property: @"key4"]];
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: withoutPrecision
                                                                           as: @"withoutPrecision"],
                                             [CBLQuerySelectResult expression: withPrecision
                                                                           as: @"withPrecision"]]
                                     from: kDATA_SRC_DB];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        Assert(0.33333 - [r doubleForKey: @"withoutPrecision"] < 0.0000);
        Assert(0.33333 - [r doubleForKey: @"withPrecision"] < 0.0000);
    }];
    AssertEqual(numRows, 1u);
}

- (void) testStringFunctions {
    NSString* str = @"  See you 18r  ";
    CBLMutableDocument* doc = [self createDocument:@"doc1"];
    [doc setValue: str forKey: @"greeting"];
    [self saveDocument: doc collection: self.defaultCollection];
    
    CBLQueryExpression* p = [CBLQueryExpression property: @"greeting"];
    
    // Contains:
    CBLQueryExpression* CONTAINS1 = [CBLQueryFunction contains: p substring: [CBLQueryExpression string: @"8"]];
    CBLQueryExpression* CONTAINS2 = [CBLQueryFunction contains: p substring: [CBLQueryExpression string: @"9"]];
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: CONTAINS1],
                                             [CBLQuerySelectResult expression: CONTAINS2]]
                                     from: kDATA_SRC_DB];
    
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual([r booleanAtIndex: 0], YES);
        AssertEqual([r booleanAtIndex: 1], NO);
    }];
    AssertEqual(numRows, 1u);
    
    // Length:
    CBLQueryExpression* LENGTH = [CBLQueryFunction length: p];
    q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: LENGTH]]
                           from: kDATA_SRC_DB];
    
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual([r integerAtIndex: 0], (NSInteger)str.length);
    }];
    AssertEqual(numRows, 1u);
    
    // Lower, Ltrim, Rtrim, Trim, Upper:
    CBLQueryExpression* LOWER = [CBLQueryFunction lower: p];
    CBLQueryExpression* LTRIM = [CBLQueryFunction ltrim: p];
    CBLQueryExpression* RTRIM = [CBLQueryFunction rtrim: p];
    CBLQueryExpression* TRIM = [CBLQueryFunction trim: p];
    CBLQueryExpression* UPPER = [CBLQueryFunction upper: p];
    
    q = [CBLQueryBuilder select: @[[CBLQuerySelectResult expression: LOWER],
                                   [CBLQuerySelectResult expression: LTRIM],
                                   [CBLQuerySelectResult expression: RTRIM],
                                   [CBLQuerySelectResult expression: TRIM],
                                   [CBLQuerySelectResult expression: UPPER]]
                           from: kDATA_SRC_DB];
    
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        
        AssertEqualObjects([r stringAtIndex: 0], [str lowercaseString]);
        AssertEqualObjects([r stringAtIndex: 1], @"See you 18r  ");
        AssertEqualObjects([r stringAtIndex: 2], @"  See you 18r");
        AssertEqualObjects([r stringAtIndex: 3], @"See you 18r");
        AssertEqualObjects([r stringAtIndex: 4], [str uppercaseString]);
    }];
    AssertEqual(numRows, 1u);
}

- (void) testQuantifiedOperators {
    [self loadJSONResource: @"names_100"];
    
    CBLQueryExpression* DOC_ID = [CBLQueryMeta id];
    CBLQuerySelectResult* S_DOC_ID = [CBLQuerySelectResult expression: DOC_ID];
    
    CBLQueryExpression* LIKES  = [CBLQueryExpression property: @"likes"];
    CBLQueryVariableExpression* LIKE = [CBLQueryArrayExpression variableWithName: @"LIKE"];
    
    // ANY:
    CBLQuery* q = [CBLQueryBuilder select: @[S_DOC_ID]
                                     from: kDATA_SRC_DB
                                    where: [CBLQueryArrayExpression any: LIKE
                                                                     in: LIKES
                                                              satisfies: [LIKE equalTo: [CBLQueryExpression string: @"climbing"]]]];
    
    NSLog(@"%@", [q explain: nil]);
    
    
    NSArray* expected = @[@"doc-017", @"doc-021", @"doc-023", @"doc-045", @"doc-060"];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqualObjects([r stringAtIndex: 0], expected[n-1]);
    }];
    
    AssertEqual(numRows, expected.count);
    
    // EVERY:
    q = [CBLQueryBuilder select: @[S_DOC_ID]
                           from: kDATA_SRC_DB
                          where: [CBLQueryArrayExpression every: LIKE
                                                             in: LIKES
                                                      satisfies: [LIKE equalTo: [CBLQueryExpression string: @"taxes"]]]];
    numRows = [self verifyQuery: q
                   randomAccess: YES
                           test: ^(uint64_t n, CBLQueryResult* r) {
        if (n == 1) AssertEqualObjects([r stringAtIndex: 0], @"doc-007");
    }];
    
    AssertEqual(numRows, 42u);
    
    // ANY AND EVERY
    q = [CBLQueryBuilder select: @[S_DOC_ID]
                           from: kDATA_SRC_DB
                          where: [CBLQueryArrayExpression anyAndEvery: LIKE
                                                                   in: LIKES
                                                            satisfies: [LIKE equalTo: [CBLQueryExpression string: @"taxes"]]]];
    numRows = [self verifyQuery: q randomAccess: YES test: ^(uint64_t n, CBLQueryResult* r) { }];
    AssertEqual(numRows, 0u);
}

- (void) testQuantifiedOperatorVariableKeyPath {
    NSArray* data = @[
                      @[@{@"city": @"San Francisco"}, @{@"city": @"Palo Alto"}, @{@"city": @"San Jose"}],
                      @[@{@"city": @"Mountain View"}, @{@"city": @"Palo Alto"}, @{@"city": @"Belmont"}],
                      @[@{@"city": @"San Francisco"}, @{@"city": @"Redwood City"}, @{@"city": @"San Mateo"}]
                      ];
    
    // Create documents:
    NSInteger i = 0;
    for (NSArray* cities in data) {
        NSString* docID = [NSString stringWithFormat: @"doc-%ld", (long)i++];
        CBLMutableDocument* doc = [self createDocument: docID];
        [doc setValue: cities forKey: @"paths"];
        
        NSData* d = [NSJSONSerialization dataWithJSONObject: [doc toDictionary] options: 0 error: nil];
        NSString* str = [[NSString alloc] initWithData: d encoding:NSUTF8StringEncoding];
        NSLog(@"%@", str);
        [self saveDocument: doc collection: self.defaultCollection];
    }
    
    CBLQueryExpression* DOC_ID  = [CBLQueryMeta id];
    CBLQuerySelectResult* S_DOC_ID = [CBLQuerySelectResult expression: DOC_ID];
    
    CBLQueryExpression* PATHS  = [CBLQueryExpression property: @"paths"];
    CBLQueryVariableExpression* PATH  = [CBLQueryArrayExpression variableWithName: @"path"];
    CBLQueryVariableExpression* PATH_CITY  = [CBLQueryArrayExpression variableWithName: @"path.city"];
    CBLQueryExpression* where = [CBLQueryArrayExpression any: PATH
                                                          in: PATHS
                                                   satisfies: [PATH_CITY equalTo: [CBLQueryExpression string: @"San Francisco"]]];
    
    CBLQuery* q = [CBLQueryBuilder select: @[S_DOC_ID]
                                     from: kDATA_SRC_DB
                                    where: where];
    
    NSArray* expected = @[@"doc-0", @"doc-2"];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: NO
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqualObjects([r stringAtIndex: 0], expected[n-1]);
    }];
    
    AssertEqual(numRows, expected.count);
}

#pragma mark - Collation

- (void) testGenerateJSONCollation {
    NSArray* collations =
    @[[CBLQueryCollation asciiWithIgnoreCase: NO],
      [CBLQueryCollation asciiWithIgnoreCase: YES],
      [CBLQueryCollation unicodeWithLocale: nil   ignoreCase: NO  ignoreAccents: NO],
      [CBLQueryCollation unicodeWithLocale: nil   ignoreCase: YES ignoreAccents: NO],
      [CBLQueryCollation unicodeWithLocale: nil   ignoreCase: YES ignoreAccents: YES],
      [CBLQueryCollation unicodeWithLocale: @"en" ignoreCase: NO  ignoreAccents: NO],
      [CBLQueryCollation unicodeWithLocale: @"en" ignoreCase: YES ignoreAccents: NO],
      [CBLQueryCollation unicodeWithLocale: @"en" ignoreCase: YES ignoreAccents: YES]];
    
    NSString* deviceLocale = [NSLocale currentLocale].localeIdentifier;
    NSArray* expected =
    @[
      @{@"UNICODE": @(NO),  @"LOCALE": [NSNull null] ,@"CASE": @(YES), @"DIAC": @(YES)},
      @{@"UNICODE": @(NO),  @"LOCALE": [NSNull null] ,@"CASE": @(NO) , @"DIAC": @(YES)},
      @{@"UNICODE": @(YES), @"LOCALE": deviceLocale  ,@"CASE": @(YES), @"DIAC": @(YES)},
      @{@"UNICODE": @(YES), @"LOCALE": deviceLocale  ,@"CASE": @(NO),  @"DIAC": @(YES)},
      @{@"UNICODE": @(YES), @"LOCALE": deviceLocale  ,@"CASE": @(NO),  @"DIAC": @(NO)},
      @{@"UNICODE": @(YES), @"LOCALE": @"en"         ,@"CASE": @(YES), @"DIAC": @(YES)},
      @{@"UNICODE": @(YES), @"LOCALE": @"en"         ,@"CASE": @(NO),  @"DIAC": @(YES)},
      @{@"UNICODE": @(YES), @"LOCALE": @"en"         ,@"CASE": @(NO),  @"DIAC": @(NO)}
      ];
    
    NSInteger i = 0;
    for (CBLQueryCollation* c in collations) {
        AssertEqualObjects([c asJSON], expected[i++]);
    }
}

- (void) testUnicodeCollationWithLocale {
    NSArray* letters = @[@"B", @"A", @"Z", @"Å"];
    for (NSString* letter in letters) {
        CBLMutableDocument* doc = [self createDocument];
        [doc setValue: letter forKey: @"string"];
        [self saveDocument: doc collection: self.defaultCollection];
    }
    
    CBLQueryExpression* STRING = [CBLQueryExpression property: @"string"];
    CBLQuerySelectResult* S_STRING = [CBLQuerySelectResult expression: STRING];
    
    // Without locale:
    CBLQueryCollation* NO_LOCALE = [CBLQueryCollation unicodeWithLocale: nil
                                                             ignoreCase: NO
                                                          ignoreAccents: NO];
    CBLQuery* q = [CBLQueryBuilder select: @[S_STRING]
                                     from: kDATA_SRC_DB
                                    where: nil
                                  orderBy: @[[CBLQueryOrdering expression: [STRING collate: NO_LOCALE]]]];
    
    NSArray* expected = @[@"A", @"Å", @"B", @"Z"];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqualObjects([r stringAtIndex: 0], expected[n-1]);
    }];
    
    AssertEqual(numRows, expected.count);
    
    // With locale:
    CBLQueryCollation* WITH_LOCALE = [CBLQueryCollation unicodeWithLocale: @"se"
                                                               ignoreCase: NO
                                                            ignoreAccents: NO];
    q = [CBLQueryBuilder select: @[S_STRING]
                           from: kDATA_SRC_DB
                          where: nil
                        orderBy: @[[CBLQueryOrdering expression: [STRING collate: WITH_LOCALE]]]];
    
    expected = @[@"A", @"B", @"Z", @"Å"];
    numRows = [self verifyQuery: q
                   randomAccess: NO
                           test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqualObjects([r stringAtIndex: 0], expected[n-1]);
    }];
    
    AssertEqual(numRows, expected.count);
}

- (void) testCompareWithUnicodeCollation {
    id bothSensitive = [CBLQueryCollation unicodeWithLocale: nil ignoreCase: NO ignoreAccents: NO];
    id accentSensitive = [CBLQueryCollation unicodeWithLocale: nil ignoreCase: YES ignoreAccents: NO];
    id caseSensitive = [CBLQueryCollation unicodeWithLocale: nil ignoreCase: NO ignoreAccents: YES];
    id noSensitive = [CBLQueryCollation unicodeWithLocale: nil ignoreCase: YES ignoreAccents: YES];
    
    NSArray* testData =
    @[// Edge cases: empty and 1-char strings:
      @[@"", @"", @YES, bothSensitive],
      @[@"", @"a", @NO, bothSensitive],
      @[@"a", @"a", @YES, bothSensitive],
      
      // Case sensitive: lowercase come first by unicode rules:
      @[@"a", @"A", @NO, bothSensitive],
      @[@"abc", @"abc", @YES, bothSensitive],
      @[@"Aaa", @"abc", @NO, bothSensitive],
      @[@"abc", @"abC", @NO, bothSensitive],
      @[@"AB", @"abc", @NO, bothSensitive],
      
      // Case insenstive:
      @[@"ABCDEF", @"ZYXWVU", @NO, accentSensitive],
      @[@"ABCDEF", @"Z", @NO, accentSensitive],
      
      @[@"a", @"A", @YES, accentSensitive],
      @[@"abc", @"ABC", @YES, accentSensitive],
      @[@"ABA", @"abc", @NO, accentSensitive],
      
      @[@"commonprefix1", @"commonprefix2", @NO, accentSensitive],
      @[@"commonPrefix1", @"commonprefix2", @NO, accentSensitive],
      
      @[@"abcdef", @"abcdefghijklm", @NO, accentSensitive],
      @[@"abcdeF", @"abcdefghijklm", @NO, accentSensitive],
      
      // Now bring in non-ASCII characters:
      @[@"a", @"á", @NO, accentSensitive],
      @[@"", @"á", @NO, accentSensitive],
      @[@"á", @"á", @YES, accentSensitive],
      @[@"•a", @"•A", @YES, accentSensitive],
      
      @[@"test a", @"test á", @NO, accentSensitive],
      @[@"test á", @"test b", @NO, accentSensitive],
      @[@"test á", @"test Á", @YES, accentSensitive],
      @[@"test á1", @"test Á2", @NO, accentSensitive],
      
      // Case sensitive, diacritic sensitive:
      @[@"ABCDEF", @"ZYXWVU", @NO, bothSensitive],
      @[@"ABCDEF", @"Z", @NO, bothSensitive],
      @[@"a", @"A", @NO, bothSensitive],
      @[@"abc", @"ABC", @NO, bothSensitive],
      @[@"•a", @"•A", @NO, bothSensitive],
      @[@"test a", @"test á", @NO, bothSensitive],
      @[@"Ähnlichkeit", @"apple", @NO, bothSensitive], // Because 'h'-vs-'p' beats 'Ä'-vs-'a'
      @[@"ax", @"Äz", @NO, bothSensitive],
      @[@"test a", @"test Á", @NO, bothSensitive],
      @[@"test Á", @"test e", @NO, bothSensitive],
      @[@"test á", @"test Á", @NO, bothSensitive],
      @[@"test á", @"test b", @NO, bothSensitive],
      @[@"test u", @"test Ü", @NO, bothSensitive],
      
      // Case sensitive, diacritic insensitive
      @[@"abc", @"ABC", @NO, caseSensitive],
      @[@"test á", @"test a", @YES, caseSensitive],
      @[@"test á", @"test A", @NO, caseSensitive],
      @[@"test á", @"test b", @NO, caseSensitive],
      @[@"test á", @"test Á", @NO, caseSensitive],
      
      // Case and diacritic insensitive
      @[@"test á", @"test Á", @YES, noSensitive]
      ];
    
    for (NSArray* data in testData) {
        CBLMutableDocument* doc = [self createDocument];
        [doc setValue: data[0] forKey: @"value"];
        [self saveDocument: doc collection: self.defaultCollection];
        
        CBLQueryExpression* VALUE = [CBLQueryExpression property: @"value"];
        CBLQueryExpression* comparison = [data[2] boolValue] ?
        [[VALUE collate: data[3]] equalTo: [CBLQueryExpression value: data[1]]] :
        [[VALUE collate: data[3]] lessThan: [CBLQueryExpression value: data[1]]];
        
        // NSLog(@"Compare %@ and %@, result = %@", data[0], data[1], data[2]);
        
        CBLQuery* q = [CBLQueryBuilder select: @[]
                                         from: kDATA_SRC_DB
                                        where: comparison];
        uint64_t numRows = [self verifyQuery: q
                                randomAccess: NO
                                        test: ^(uint64_t n, CBLQueryResult* r) { }];
        AssertEqual(numRows, 1u);
        
        Assert([self.defaultCollection deleteDocument: doc error: nil]);
    }
}

#pragma mark - Results

- (void) testQueryResult {
    [self loadNumbers: 100];
    [self loadStudents];
    
    CBLQueryExpression* FNAME = [CBLQueryExpression property: @"name.first"];
    CBLQueryExpression* LNAME = [CBLQueryExpression property: @"name.last"];
    CBLQueryExpression* EMAIL = [CBLQueryExpression property: @"contact.email"];
    CBLQueryExpression* ADDRESS = [CBLQueryExpression property: @"contact.address"];
    CBLQueryExpression* CODE = [CBLQueryExpression property: @"code"];
    CBLQueryExpression* YEAR = [CBLQueryExpression property: @"year"];
    CBLQueryExpression* ID = [CBLQueryExpression property: @"id"];
    CBLQueryExpression* SCORE = [CBLQueryExpression property: @"score"];
    CBLQueryExpression* IS_FULLTIME = [CBLQueryExpression property: @"isFullTime"];
    CBLQueryExpression* START_DATE = [CBLQueryExpression property: @"startDate"];
    CBLQueryExpression* GPA = [CBLQueryExpression property: @"gpa"];
    
    CBLQuerySelectResult* S_FNAME = [CBLQuerySelectResult expression: FNAME as: @"firstname"];
    CBLQuerySelectResult* S_LNAME = [CBLQuerySelectResult expression: LNAME as: @"lastname"];
    CBLQuerySelectResult* S_EMAIL = [CBLQuerySelectResult expression: EMAIL as: @"email"];
    CBLQuerySelectResult* S_ADDRESS = [CBLQuerySelectResult expression: ADDRESS as: @"address"];
    CBLQuerySelectResult* S_CODE = [CBLQuerySelectResult expression: CODE];
    CBLQuerySelectResult* S_YEAR = [CBLQuerySelectResult expression: YEAR];
    CBLQuerySelectResult* S_ID = [CBLQuerySelectResult expression: ID];
    CBLQuerySelectResult* S_SCORE = [CBLQuerySelectResult expression: SCORE];
    CBLQuerySelectResult* S_IS_FULLTIME = [CBLQuerySelectResult expression: IS_FULLTIME];
    CBLQuerySelectResult* S_START_DATE = [CBLQuerySelectResult expression: START_DATE];
    CBLQuerySelectResult* S_GPA = [CBLQuerySelectResult expression: GPA];
    
    CBLQuery* q = [CBLQueryBuilder select: @[S_FNAME, S_LNAME, S_EMAIL, S_ADDRESS, S_CODE, S_YEAR,
                                             S_ID, S_SCORE, S_IS_FULLTIME, S_START_DATE, S_GPA]
                                     from: kDATA_SRC_DB];
    
    NSSet* keys = [NSSet setWithObjects: @"lastname", @"email", @"address", @"firstname", @"code",
                   @"year", @"id", @"score", @"isFullTime", @"startDate", @"gpa", nil];
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual(r.count, 11u);
        AssertEqualObjects([r valueForKey: @"firstname"], [r valueAtIndex: 0]);
        AssertEqualObjects(r[0].string, r[@"firstname"].string);
        AssertEqualObjects([r valueForKey: @"lastname"], [r valueAtIndex: 1]);
        AssertEqualObjects([r arrayForKey: @"email"], [r arrayAtIndex: 2]);
        AssertEqualObjects([r dictionaryForKey: @"address"], [r dictionaryAtIndex: 3]);
        AssertEqualObjects([r numberForKey: @"code"], [r numberAtIndex: 4]);
        AssertEqual([r integerForKey: @"year"], [r integerAtIndex: 5]);
        AssertEqual([r longLongForKey: @"id"], [r longLongAtIndex: 6]);
        AssertEqual([r floatForKey: @"score"], [r floatAtIndex: 7]);
        AssertEqual([r booleanForKey: @"isFullTime"], [r booleanAtIndex: 8]);
        AssertEqualObjects([r dateForKey: @"startDate"], [r dateAtIndex: 9]);
        AssertEqual([r doubleForKey: @"gpa"], [r doubleAtIndex: 10]);
        AssertEqualObjects([NSSet setWithArray: [r keys]], keys); // using set will ignore the order
    }];
    
    AssertEqual((int)numRows, 108);
}

- (void) testQueryProjectingKeys {
    [self loadNumbers: 100];
    
    CBLQueryExpression* AVG = [CBLQueryFunction avg: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* CNT = [CBLQueryFunction count: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* MIN = [CBLQueryFunction min: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* MAX = [CBLQueryFunction max: [CBLQueryExpression property: @"number1"]];
    CBLQueryExpression* SUM = [CBLQueryFunction sum: [CBLQueryExpression property: @"number1"]];
    
    NSArray* results = @[[CBLQuerySelectResult expression: AVG],
                         [CBLQuerySelectResult expression: CNT],
                         [CBLQuerySelectResult expression: MIN as: @"min"],
                         [CBLQuerySelectResult expression: MAX],
                         [CBLQuerySelectResult expression: SUM as: @"sum"]];
    
    CBLQuery* q = [CBLQueryBuilder select: results
                                     from: kDATA_SRC_DB];
    Assert(q);
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        AssertEqual(r.count, 5u);
        AssertEqual([r doubleForKey: @"$1"], [r doubleAtIndex: 0]);
        AssertEqual([r integerForKey: @"$2"], [r integerAtIndex: 1]);
        AssertEqual([r integerForKey: @"min"], [r integerAtIndex: 2]);
        AssertEqual([r integerForKey: @"$3"], [r integerAtIndex: 3]);
        AssertEqual([r integerForKey: @"sum"], [r integerAtIndex: 4]);
    }];
    
    AssertEqual(numRows, 1u);
}

- (void) testResultSetEnumeration {
    [self loadNumbers: 5];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: nil
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    NSError* error;
    CBLQueryResultSet* rs = [q execute: &error];
    Assert(rs, @"Query failed: %@", error);
    
    // Enumeration:
    NSUInteger i = 0;
    CBLQueryResult *r;
    while ((r = [rs nextObject])) {
        NSString* docID = [NSString stringWithFormat: @"doc%ld", (long)(i+1)];
        AssertEqualObjects([r valueAtIndex: 0], docID);
        i++;
    }
    AssertEqual(i, 5u);
    AssertNil([rs nextObject]);
    AssertEqual([rs allObjects].count, 0u);
    AssertEqual([rs allResults].count, 0u);
    
    // Fast enumeration:
    i = 0;
    rs = [q execute: &error];
    Assert(rs, @"Query failed: %@", error);
    
    for (r in rs) {
        NSString* docID = [NSString stringWithFormat: @"doc%ld", (long)(i+1)];
        AssertEqualObjects([r valueAtIndex: 0], docID);
        i++;
    }
    AssertEqual(i, 5u);
    AssertNil([rs nextObject]);
    AssertEqual([rs allObjects].count, 0u);
    AssertEqual([rs allResults].count, 0u);
}

- (void) testGetAllResults {
    [self loadNumbers: 5];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: nil
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    
    // Get all results:
    NSError* error;
    CBLQueryResultSet* rs = [q execute: &error];
    Assert(rs, @"Query failed: %@", error);
    
    NSUInteger i = 0;
    NSArray* results = [rs allResults];
    for (CBLQueryResult* r in results) {
        NSString* docID = [NSString stringWithFormat: @"doc%ld", (long)(i+1)];
        AssertEqualObjects([r valueAtIndex: 0], docID);
        i++;
    }
    AssertEqual(results.count, 5u);
    AssertNil([rs nextObject]);
    AssertEqual([rs allObjects].count, 0u);
    AssertEqual([rs allResults].count, 0u);
    
    // Partial enumerating then get all results:
    rs = [q execute: &error];
    Assert(rs, @"Query failed: %@", error);
    
    AssertNotNil([rs nextObject]);
    AssertNotNil([rs nextObject]);
    
    i = 0;
    results = [rs allResults];
    for (CBLQueryResult* r in results) {
        NSString* docID = [NSString stringWithFormat: @"doc%ld", (long)(i+3)];
        AssertEqualObjects([r valueAtIndex: 0], docID);
        i++;
    }
    AssertEqual(results.count, 3u);
    AssertNil([rs nextObject]);
    AssertEqual([rs allObjects].count, 0u);
    AssertEqual([rs allResults].count, 0u);
}

- (void) testMissingValue {
    CBLMutableDocument* doc1 = [self createDocument: @"doc1"];
    [doc1 setValue: @"Scott" forKey: @"name"];
    [doc1 setValue: nil forKey: @"address"];
    [self saveDocument: doc1 collection: self.defaultCollection];
    
    CBLQuery *q = [CBLQueryBuilder select: @[[CBLQuerySelectResult property: @"name"],
                                             [CBLQuerySelectResult property: @"address"],
                                             [CBLQuerySelectResult property: @"age"]]
                                     from: kDATA_SRC_DB];
    
    NSError* error;
    CBLQueryResultSet* rs = [q execute: &error];
    Assert(rs, @"Query failed: %@", error);
    
    CBLQueryResult* r = [rs nextObject];
    
    // Array:
    AssertEqual(r.count, 3u);
    AssertEqualObjects([r stringAtIndex: 0], @"Scott");
    AssertNil([r valueAtIndex: 1]);
    AssertNil([r valueAtIndex: 2]);
    AssertEqualObjects([r toArray], (@[@"Scott", [NSNull null], [NSNull null]]));
    
    // Dictionary:
    AssertEqualObjects([r stringForKey: @"name"], @"Scott");
    AssertNil([r stringForKey: @"address"]);
    Assert([r containsValueForKey: @"address"]);
    AssertNil([r stringForKey: @"age"]);
    AssertFalse([r containsValueForKey: @"age"]);
    AssertEqualObjects([r toDictionary], (@{@"name": @"Scott", @"address": [NSNull null]}));
}

- (void) testJSONEncoding {
    NSError* error;
    CBLMutableDocument* doc1 = [[CBLMutableDocument alloc] init];
    [doc1 setValue: @"string" forKey: @"string"];
    Assert([self.defaultCollection saveDocument: doc1 error: &error], @"Error when creating a document: %@", error);
    
    NSData* json;
    {
        CBLQueryExpression* expr = [[CBLQueryExpression property: @"string"] is:
                                    [CBLQueryExpression string: @"string"]];
        CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                         from: kDATA_SRC_DB
                                        where: expr];
        json = q.json;
        Assert(json);
    }
    
    // Reconstitute query from JSON data:
    CBLQuery* q = [[CBLQuery alloc] initWithDatabase: _db json: json];
    Assert(q);
    AssertEqualObjects(q.json, json);
    
    // Now test the reconstituted query:
    uint64_t numRows = [self verifyQuery: q
                            randomAccess: YES
                                    test: ^(uint64_t n, CBLQueryResult* r) {
        NSError* err;
        CBLDocument* doc = [self.defaultCollection documentWithID: [r valueAtIndex: 0] error: &err];
        AssertEqualObjects(doc.id, doc1.id);
        AssertEqualObjects([doc valueForKey: @"string"], @"string");
    }];
    AssertEqual(numRows, 1u);
}

- (void) testQueryResultArray {
    [self loadNumbers: 5];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: nil
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    NSError* error;
    CBLQueryResultSet* rs = [q execute: &error];
    Assert(rs, @"Query failed: %@", error);
    
    NSArray* allObjects = rs.allObjects;
    CBLQueryResultArray* array = [[CBLQueryResultArray alloc] initWithResultSet: rs
                                                                          count: allObjects.count];
    AssertEqual(array.count, allObjects.count);
    Assert(![[array mutableCopy] isEqual: array]);
    AssertEqual([[array objectAtIndex: 0] valueForKey: @"id"],
                [[allObjects objectAtIndex: 0] valueForKey: @"id"]);
    AssertEqual([[array objectAtIndex: 4] valueForKey: @"id"],
                [[allObjects objectAtIndex: 4] valueForKey: @"id"]);
}

#pragma mark - toJSON

- (void) testQueryJSON {
    NSError* error;
    NSString* json = [self getRickAndMortyJSON];
    CBLMutableDocument* doc = [[CBLMutableDocument alloc] initWithID: @"doc" json: json
                                                               error: &error];
    [self.defaultCollection saveDocument: doc error: &error];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID,
                                             [CBLQuerySelectResult property: @"id #2"],
                                             [CBLQuerySelectResult property: @"name"],
                                             [CBLQuerySelectResult property: @"isAlive"],
                                             [CBLQuerySelectResult property: @"species"],
                                             [CBLQuerySelectResult property: @"picture"],
                                             [CBLQuerySelectResult property: @"gender"],
                                             [CBLQuerySelectResult property: @"registered"],
                                             [CBLQuerySelectResult property: @"latitude"],
                                             [CBLQuerySelectResult property: @"longitude"],
                                             [CBLQuerySelectResult property: @"aka"],
                                             [CBLQuerySelectResult property: @"family"],
                                             [CBLQuerySelectResult property: @"origin"]]
                                     from: kDATA_SRC_DB
                                    where: nil];
    
    CBLQueryResultSet* rs = [q execute: &error];
    Assert(rs, @"Query failed: %@", error);
    
    CBLQueryResult* r = rs.allResults.firstObject;
    NSMutableDictionary* temp = [[json toJSONObj] mutableCopy];
    temp[@"id"] = @"doc";
    AssertEqualObjects([[r toJSON] toJSONObj], temp);
}

#pragma mark - Value Expression

- (void) testValueExpressionUnsupportedValueType {
    NSData* data = [[NSData alloc] init];
    [self expectException: NSInternalInconsistencyException in:^{
        CBLValueExpression* v = [[CBLValueExpression alloc] initWithValue: data];
        AssertNil(v);
    }];
}

- (void) testValueExpression {
    CBLValueExpression* v = [[CBLValueExpression alloc] initWithValue: nil];
    AssertEqualObjects([v asJSON], [NSNull null]);
    
    v = [[CBLValueExpression alloc] initWithValue: [NSDate dateWithTimeIntervalSince1970: 1]];
    AssertEqualObjects([v asJSON], @"1970-01-01T00:00:01.000Z");
    
    v = [[CBLValueExpression alloc] initWithValue: [NSDictionary dictionaryWithObjectsAndKeys:
                                                    @"value101", @"key101", nil]];
    AssertEqualObjects([v asJSON], @{@"key101": @"value101"});
    
    NSArray* expectedResult= @[ @"[]", @"item1", @"item2" ];
    v = [[CBLValueExpression alloc] initWithValue: [NSArray arrayWithObjects:
                                                    @"item1", @"item2", nil]];
    AssertEqualObjects([v asJSON], expectedResult);
    
    v = [[CBLValueExpression alloc] initWithValue: [CBLQueryExpression number: @21]];
    AssertEqualObjects([v asJSON], @21);
}

- (void) testUnaryQueryExpression {
    NSDate* nw = [NSDate date];
    CBLMutableDocument* doc1 = [self createDocument];
    [doc1 setDate: nw forKey: @"now"];
    [self saveDocument: doc1 collection: self.defaultCollection];
    
    CBLMutableDocument* doc2 = [self createDocument];
    [self saveDocument: doc2 collection: self.defaultCollection];
    
    CBLUnaryExpression* notNull;
    CBLUnaryExpression* notMiss;
    CBLQueryExpression* propNow = [CBLQueryExpression property: @"now"];
    notNull = [[CBLUnaryExpression alloc] initWithExpression: propNow type: CBLUnaryTypeNotNull];
    notMiss = [[CBLUnaryExpression alloc] initWithExpression: propNow type: CBLUnaryTypeNotMissing];
    
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult all]]
                                     from: kDATA_SRC_DB
                                    where: [notNull orExpression: notMiss]];
    uint64_t rows = [self verifyQuery: q randomAccess: YES
                                 test: ^(uint64_t n, CBLQueryResult * _Nonnull result) {
                                     NSDate* savedDate = [[result dictionaryAtIndex: 0]
                                                          dateForKey: @"now"];
                                     Assert([nw timeIntervalSinceDate: savedDate] < 0.001);
                                 }];
    AssertEqual(rows, 1u);
    
    // check same result is produced with isValued.
    q = [CBLQueryBuilder select: @[[CBLQuerySelectResult all]]
                           from: kDATA_SRC_DB
                          where: [propNow isValued]];

    rows = [self verifyQuery: q randomAccess: YES
                        test: ^(uint64_t n, CBLQueryResult * _Nonnull result) {
                            NSDate* savedDate = [[result dictionaryAtIndex: 0] dateForKey: @"now"];
                            Assert([nw timeIntervalSinceDate: savedDate] < 0.001);
                        }];
    AssertEqual(rows, 1u);
    
    q = [CBLQueryBuilder select: @[[CBLQuerySelectResult all]]
                           from: kDATA_SRC_DB
                          where: [propNow isValued]];
    rows = [self verifyQuery: q randomAccess: YES
                        test: ^(uint64_t n, CBLQueryResult * _Nonnull result) {
                            NSDate* savedDate = [[result dictionaryAtIndex: 0] dateForKey: @"now"];
                            Assert([nw timeIntervalSinceDate: savedDate] < 0.001);
                        }];
    AssertEqual(rows, 1u);
}

#pragma mark - N1QL

- (void) testN1QLQuerySanity {
    CBLMutableDocument* doc = [self createDocument:@"doc1"];
    [doc setValue: @"Jerry" forKey: @"firstName"];
    [doc setValue: @"Ice Cream" forKey: @"lastName"];
    [self saveDocument: doc collection: self.defaultCollection];
    
    doc = [self createDocument:@"doc2"];
    [doc setValue: @"Ben" forKey: @"firstName"];
    [doc setValue: @"Ice Cream" forKey: @"lastName"];
    [self saveDocument: doc collection: self.defaultCollection];
    
    [self validateN1QLQuery: $sprintf(@"SELECT firstName, lastName FROM %@", self.db.name)];
    [self validateN1QLQuery: @"SELECT firstName, lastName FROM _"];
    [self validateN1QLQuery: @"SELECT firstName, lastName FROM _default"];
}

- (void) testInvalidN1QLQuery {
    [self ignoreException: ^{
        NSError* error;
        CBLQuery* q = [self.db createQuery: @"SELECT firstName, lastName" error: &error];
        AssertNil(q);
        AssertEqual(error.domain, CBLErrorDomain);
        AssertEqual(error.code, CBLErrorInvalidQuery);
    }];
}

- (void) validateN1QLQuery: (NSString*)queryString {
    NSError* error;
    CBLQuery* q = [self.db createQuery: queryString error: &error];
    AssertNotNil(q);
    AssertNil(error);
    
    NSArray<CBLQueryResult*>* result = [q execute: &error].allResults;
    AssertEqual(result.count, 2);
    AssertEqualObjects([result[0] stringForKey: @"firstName"], @"Jerry");
    AssertEqualObjects([result[0] stringForKey: @"lastName"], @"Ice Cream");
    AssertEqualObjects([result[1] stringForKey: @"firstName"], @"Ben");
    AssertEqualObjects([result[1] stringForKey: @"lastName"], @"Ice Cream");
}

#pragma mark - LiveQuery

- (void) testLiveQuery {
    [self loadNumbers: 100];
    
    __block int count = 0;
    XCTestExpectation* first = [self expectationWithDescription: @"1st change"];
    XCTestExpectation* second = [self expectationWithDescription: @"2nd change"];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: [[CBLQueryExpression property: @"number1"] lessThan: [CBLQueryExpression integer: 10]]
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    
    id token = [q addChangeListener: ^(CBLQueryChange* change) {
        count++;
        AssertNotNil(change.query);
        AssertNil(change.error);
        NSArray<CBLQueryResult*>* rows = [change.results allObjects];
        if (count == 1) {
            AssertEqual(rows.count, 9u);
            [first fulfill];
        } else {
            NSError* error;
            AssertEqual(rows.count, 10u);
            CBLDocument* doc = [self.defaultCollection documentWithID: [rows[0] valueAtIndex: 0] error: &error];
            AssertEqualObjects([doc valueForKey: @"number1"], @(-1));
            [second fulfill];
        }
    }];
    
    [self waitForExpectations: @[first] timeout: kExpTimeout];
    [self createDocNumbered: -1 of: 100];
    
    [self waitForExpectations: @[second] timeout: kExpTimeout];
    [token remove];
}

- (void) testLiveQueryNoUpdate {
    [self loadNumbers: 100];
    
    __block int count = 0;
    XCTestExpectation* first = [self expectationWithDescription: @"1st change"];
    CBLQuery* q = [CBLQueryBuilder select: @[]
                                     from: kDATA_SRC_DB
                                    where: [[CBLQueryExpression property: @"number1"] lessThan: [CBLQueryExpression integer: 10]]
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    
    id token = [q addChangeListener:^(CBLQueryChange* change) {
        count++;
        AssertNotNil(change.query);
        AssertNil(change.error);
        NSArray<CBLQueryResult*>* rows = [change.results allObjects];
        if (count == 1) {
            AssertEqual(rows.count, 9u);
            [first fulfill];
        } else {
            XCTFail(@"Unexpected update from LiveQuery");
        }
    }];
    
    [self waitForExpectations: @[first] timeout: kExpTimeout];
    
    // This change will not affect the query results because 'number1 < 10' is not true.
    [self createDocNumbered: 111 of: 100];
    
    
    // Wait 2 seconds, then fulfil the expectation:
    XCTestExpectation *x = [self expectationWithDescription: @"Timeout"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                       [x fulfill];
                   });
    
    NSLog(@"Waiting for 2secs...");
    [self waitForExpectations: @[x] timeout: kExpTimeout];
    NSLog(@"Done!");
    
    AssertEqual(count, 1);
    [token remove];
}

// CBSE-15957: Crash when creating multiple live queries concurrently
- (void) testCreateLiveQueriesConcurrently {
    // Create 1000 docs:
    [self loadNumbers: 1000];
    
    // Create two live queries then remove them concurrently:
    XCTestExpectation* remExp = [self expectationWithDescription: @"Listener Removed"];
    remExp.expectedFulfillmentCount = 2;
    
    dispatch_queue_t queue = dispatch_queue_create("query-queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 2; i++) {
        dispatch_async(queue, ^{
            XCTestExpectation* exp = [self expectationWithDescription: @"Change Received"];
            NSError* error;
            NSString* str = [NSString stringWithFormat: @"select * from _ where number1 < %d", (i * 200)];
            CBLQuery* query = [self.db createQuery: str error: &error];
            AssertNotNil(query);
            id token = [query addChangeListener:^(CBLQueryChange* change) {
                [exp fulfill];
            }];
            [self waitForExpectations: @[exp] timeout: kExpTimeout];
            [token remove];
            [remExp fulfill];
        });
    }
    
    [self waitForExpectations: @[remExp] timeout: kExpTimeout];
}

/**
 When adding a second listener after the first listener is notified, the second listener
 should get the change (current result).
 */
- (void) testLiveQuerySecondListenerReturnsResultsImmediately {
    [self createDocNumbered: 7 of: 10];
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult property: @"number1"]]
                                     from: kDATA_SRC_DB
                                    where: [[CBLQueryExpression property: @"number1"] lessThan: [CBLQueryExpression integer: 10]]
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    
    // first listener
    XCTestExpectation* first = [self expectationWithDescription: @"1st change"];
    __block int count = 0;
    id token1 = [q addChangeListener: ^(CBLQueryChange* change) {
        count++;
        NSArray<CBLQueryResult*>* rows = [change.results allObjects];
        AssertEqual(rows.count, 1);
        AssertEqual([rows[0] integerAtIndex: 0], 7);
        [first fulfill];
    }];
    [self waitForExpectations: @[first] timeout: kExpTimeout];
    
    XCTestExpectation* firstA = [self expectationWithDescription: @"2nd listener with 1st result"];
    __block int count2 = 0;
    id token2 = [q addChangeListener: ^(CBLQueryChange* change) {
        count2++;
        NSArray<CBLQueryResult*>* rows = [change.results allObjects];
        AssertEqual(rows.count, 1);
        AssertEqual([rows[0] integerAtIndex: 0], 7);
        [firstA fulfill];
    }];
    [self waitForExpectations: @[firstA] timeout: kExpTimeout];
    AssertEqual(count2, 1);
    AssertEqual(count, 1);
    [token1 remove];
    [token2 remove];
}

- (void) testLiveQueryReturnsEmptyResultSet {
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult property: @"number1"]]
                                     from: kDATA_SRC_DB];
    XCTestExpectation* first = [self expectationWithDescription: @"1st change"];
    id token = [q addChangeListener: ^(CBLQueryChange* change) {
        NSArray<CBLQueryResult*>* rows = [change.results allObjects];
        AssertEqual(rows.count, 0);
        [first fulfill];
    }];
    [self waitForExpectations: @[first] timeout: kExpTimeout];
    [token remove];
}

/**
 When having more than one listeners, each listener should have independent result sets.
 This means that both listeners should be able to iterate separately through the result correct
 when getting the same change notified.
 */
- (void) testLiveQueryMultipleListenersReturnIndependentResultSet {
    [self loadNumbers: 100];
    CBLQuery* q = [CBLQueryBuilder select: @[[CBLQuerySelectResult property: @"number1"]]
                                     from: kDATA_SRC_DB
                                    where: [[CBLQueryExpression property: @"number1"] lessThan: [CBLQueryExpression integer: 10]]
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    
    XCTestExpectation* first1 = [self expectationWithDescription: @"1st change - 1st listener"];
    XCTestExpectation* second1 = [self expectationWithDescription: @"2nd change - 1st listener"];
    XCTestExpectation* first2 = [self expectationWithDescription: @"1st change - 2nd listener"];
    XCTestExpectation* second2 = [self expectationWithDescription: @"2nd change - 2nd listener"];
    
    __block NSUInteger count1 = 0;
    id token1 = [q addChangeListener: ^(CBLQueryChange* change) {
        count1++;
        NSUInteger num = 0;
        if (count1 == 1) {
            for (CBLQueryResult* result in change.results) {
                num++;
                Assert([result integerAtIndex: 0] < 10);
            }
            AssertEqual(num, 9);
            [first1 fulfill];
        } else if (count1 == 2) {
            for (CBLQueryResult* result in change.results) {
                num++;
                Assert([result integerAtIndex: 0] < 10);
            }
            AssertEqual(num, 10);
            [second1 fulfill];
        }
    }];
    
    __block NSUInteger count2 = 0;
    id token2 = [q addChangeListener: ^(CBLQueryChange* change) {
        count2++;
        NSUInteger num = 0;
        if (count2 == 1) {
            for (CBLQueryResult* result in change.results) {
                num++;
                Assert([result integerAtIndex: 0] < 10);
            }
            AssertEqual(num, 9);
            [first2 fulfill];
        } else if (count2 == 2) {
            for (CBLQueryResult* result in change.results) {
                num++;
                Assert([result integerAtIndex: 0] < 10);
            }
            AssertEqual(num, 10);
            [second2 fulfill];
        }
    }];
    
    [self waitForExpectations:@[first1, first2] timeout: kExpTimeout];
    [self createDocNumbered: -1 of: 100];
    
    [self waitForExpectations: @[second1, second2] timeout: kExpTimeout];
    [token1 remove];
    [token2 remove];
}

- (void) testLiveQueryUpdateQueryParam {
    [self loadNumbers: 100];
    CBLQuery* q = [CBLQueryBuilder select: @[kDOCID]
                                     from: kDATA_SRC_DB
                                    where: [[CBLQueryExpression property: @"number1"] lessThan: [CBLQueryExpression parameterNamed: @"param1"]]
                                  orderBy: @[[CBLQueryOrdering property: @"number1"]]];
    // set the param
    CBLQueryParameters* params = [[CBLQueryParameters alloc] init];
    [params setInteger: 10 forName: @"param1"];
    [q setParameters: params];
    
    XCTestExpectation* first = [self expectationWithDescription: @"1st change"];
    XCTestExpectation* second = [self expectationWithDescription: @"2nd change"];
    __block int count = 0;
    id token = [q addChangeListener: ^(CBLQueryChange* change) {
        count++;
        NSArray<CBLQueryResult*>* rows = [change.results allObjects];
        if (count == 1) {
            AssertEqual(rows.count, 9);
            [first fulfill];
        } else if (count == 2) {
            AssertEqual(rows.count, 4);
            [second fulfill];
        }
    }];
    [self waitForExpectations: @[first] timeout: kExpTimeout];
    CBLQueryParameters* updated = [[CBLQueryParameters alloc] init];
    [updated setInteger: 5 forName: @"param1"];
    [q setParameters: updated];
    
    [self waitForExpectations: @[second] timeout: 10.0];
    AssertEqual(count, 2);
    [token remove];
}

// CBSE-16662, CBL-5631: thread_mutex crash issue
// This test is a sanity test to check that no crash happens but as the issue was a race condition
// in LiteCore, the test might pass without hitting the issue.
- (void) testCreateAndRemoveLiveQueriesConcurrently {
    [self loadNumbers: 1000];
    
    NSError* error;
    
    NSLock *lock = [[NSLock alloc] init];
    dispatch_queue_t queue = dispatch_queue_create("query-queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t listenerQueue = dispatch_queue_create("query-listener-queue", DISPATCH_QUEUE_CONCURRENT);
    
    CBLQuery* query = [self.db createQuery: @"select * from _ where number1 < 500" error: &error];
    AssertNotNil(query);
    
    NSMutableArray<id<CBLListenerToken>>* tokens = [NSMutableArray array];
    NSMutableArray<XCTestExpectation*>* expectations = [NSMutableArray array];
    
    for (int i = 1; i <= 1000; i++) {
        XCTestExpectation* exp = [self expectationWithDescription: [NSString stringWithFormat: @"exp %d", i]];
        [expectations addObject: exp];
        
        dispatch_async(queue, ^{
            if (i % 2 == 1) {
                id token = [query addChangeListenerWithQueue: listenerQueue listener:^(CBLQueryChange *change) { }];
                [lock lock];
                [tokens addObject: token];
                [lock unlock];
            } else {
                [lock lock];
                if ([tokens count] > 0) {
                    id<CBLListenerToken> token = [tokens objectAtIndex: 0];
                    [tokens removeObjectAtIndex: 0];
                    [token remove];
                }
                [lock unlock];
            }
            [exp fulfill];
        });
    }
    
    [self waitForExpectations: expectations timeout: 10];
    
    // Remove any tokens left:
    for (id<CBLListenerToken> token in tokens) {
        [token remove];
    }
    [tokens removeAllObjects];
    
    // Wait for listener queue to get drained:
    [NSThread sleepForTimeInterval: 5];
}

- (void) testLiveQueryNoChangesNotifiedAfterRemoveListenerToken {
    // Create 1000 docs:
    [self createDocNumbered: 1 of: 100];
    
    NSError *error;
    NSString* str = [NSString stringWithFormat: @"select * from _ where number1 < 300"];
    CBLQuery* query = [self.db createQuery: str error: &error];
    
    __block BOOL tokenRemoved = NO;
    
    XCTestExpectation* changedExp = [self expectationWithDescription: @"Changed"];
    XCTestExpectation* noChangedExp = [self expectationWithDescription: @"No Changed"];
    noChangedExp.inverted = YES;
    
    id token = [query addChangeListener:^(CBLQueryChange* change) {
        if (tokenRemoved) {
            [noChangedExp fulfill];
        } else {
            [changedExp fulfill];
        }
    }];
    
    [self waitForExpectations: @[changedExp] timeout: kExpTimeout];
    
    // Remove the token:
    tokenRemoved = YES;
    [token remove];
    
    // Add more docs:
    [self createDocNumbered: 101 of: 200];
    
    // Wait for no changes notified:
    [self waitForExpectations: @[noChangedExp] timeout: 3.0];
}

#ifdef DEBUG

// CBL-5659 : Invalidated context may be used in query observer callback
// This tests that the callback will not be called without a crash.
- (void) testLiveQueryNoDelayedChangesNotifiedAfterRemoveListenerToken {
    // Create 1000 docs:
    [self createDocNumbered: 1 of: 100];
    
    __block BOOL tokenRemoved = NO;
    
    XCTestExpectation* changedExp = [self expectationWithDescription: @"Changed"];
    XCTestExpectation* noChangedExp = [self expectationWithDescription: @"No Changed"];
    noChangedExp.inverted = YES;
    
    // Execute the code in an autoreleasepool so that the internal CBLQueryObserver will be released
    // immediately right after the token is removed.
    @autoreleasepool {
        NSError *error;
        NSString* str = [NSString stringWithFormat: @"select * from _ where number1 < 300"];
        CBLQuery* query = [self.db createQuery: str error: &error];
        
        id token = [query addChangeListener:^(CBLQueryChange* change) {
            if (tokenRemoved) {
                [noChangedExp fulfill];
            } else {
                [changedExp fulfill];
            }
        }];
        
        [self waitForExpectations: @[changedExp] timeout: kExpTimeout];
        
        // Inject some delay in C4QueryObserverCallback:
        [CBLQueryObserver setC4QueryObserverCallbackDelayInterval: 3.0];
        
        // Add more docs:
        [self createDocNumbered: 101 of: 200];
        
        // Wait a little for the query to pickup the change:
        [NSThread sleepForTimeInterval: 1.0];
        
        // Remove the token:
        tokenRemoved = YES;
        [token remove];
    }
    
    // Wait for no changes notified:
    [self waitForExpectations: @[noChangedExp] timeout: kExpTimeout];
}

#endif

@end
